package org.n3r.idworker.strategy;

import java.io.File;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Properties;
import java.util.Random;
import org.n3r.idworker.WorkerIdStrategy;
import org.n3r.idworker.utils.HttpReq;
import org.n3r.idworker.utils.Ip;
import org.n3r.idworker.utils.Props;
import org.n3r.idworker.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DefaultWorkerIdStrategy implements WorkerIdStrategy {

  static long workerIdBits = 10L;
  static long maxWorkerId = -1L ^ (-1L << workerIdBits);
  static Random random = new SecureRandom();

  public static final WorkerIdStrategy instance = new DefaultWorkerIdStrategy();

  private final Properties props =
      Props.tryProperties("idworker-client.properties", Utils.DOT_IDWORKERS);
  private final String idWorkerServerUrl =
      props.getProperty("server.address", "http://id.worker.server:18001");

  String userName = System.getProperty("user.name");

  String ipDotUsername = Ip.ip + "." + userName;
  String ipudotlock = ipDotUsername + ".lock.";
  int workerIdIndex = ipudotlock.length();
  long workerId;
  FileLock fileLock;

  Logger logger = LoggerFactory.getLogger(DefaultWorkerIdStrategy.class);
  private boolean inited;


  private void init() {
    workerId = findAvailWorkerId();
    if (workerId >= 0) {
      destroyFileLockWhenShutdown();
      startSyncThread();
    } else {
      syncWithWorkerIdServer();
      workerId = findAvailWorkerId();
        if (workerId < 0) {
            workerId = increaseWithWorkerIdServer();
        }
    }

      if (workerId < 0) {
          workerId = tryToCreateOnIp();
      }
    if (workerId < 0) {
      logger.warn("DANGEROUS!!! Try to use random worker id.");
      workerId = tryToRandomOnIp(); // Try avoiding! it could cause duplicated
    }

    if (workerId < 0) {
      logger.warn("the world may be ended!");
      throw new RuntimeException("the world may be ended");
    }
  }

  private void destroyFileLockWhenShutdown() {
    Runtime.getRuntime().addShutdownHook(new Thread() {
      @Override
      public void run() {
        fileLock.destroy();
      }
    });
  }

  private void startSyncThread() {
    new Thread() {
      @Override
      public void run() {
        syncWithWorkerIdServer();
      }
    }.start();
  }

  private long increaseWithWorkerIdServer() {
    String incId = HttpReq.get(idWorkerServerUrl)
        .req("/inc")
        .param("ipu", ipDotUsername)
        .exec();
      if (incId == null || incId.trim().isEmpty()) {
          return -1L;
      }

    long lid = Long.parseLong(incId);

    return checkAvail(lid);
  }

  private long tryToCreateOnIp() {
    long wid = Ip.lip & maxWorkerId;

    return checkAvail(wid);
  }

  private long tryToRandomOnIp() {
    long avaiWorkerId = -1L;
    long tryTimes = -1;

    while (avaiWorkerId < 0 && ++tryTimes < maxWorkerId) {
      long wid = Ip.lip & random.nextInt((int) maxWorkerId);

      avaiWorkerId = checkAvail(wid);
    }
    return avaiWorkerId;
  }

  private long checkAvail(long wid) {
    long availWorkerId = -1L;
    try {
      File idWorkerHome = Utils.createIdWorkerHome();
      new File(idWorkerHome, ipudotlock + String.format("%04d", wid)).createNewFile();
      availWorkerId = findAvailWorkerId();
    } catch (IOException e) {
      logger.warn("checkAvail error", e);
    }

    return availWorkerId;
  }

  private void syncWithWorkerIdServer() {
    String syncIds = HttpReq.get(idWorkerServerUrl).req("/sync")
        .param("ipu", ipDotUsername).param("ids", buildWorkerIdsOfCurrentIp())
        .exec();
      if (syncIds == null || syncIds.trim().isEmpty()) {
          return;
      }

    String[] syncIdsArr = syncIds.split(",");
    File idWorkerHome = Utils.createIdWorkerHome();
    for (String syncId : syncIdsArr) {
      try {
        new File(idWorkerHome, ipudotlock + syncId).createNewFile();
      } catch (IOException e) {
        logger.warn("create workerid lock file error", e);
      }
    }
  }

  private String buildWorkerIdsOfCurrentIp() {
    StringBuilder sb = new StringBuilder();
    File idWorkerHome = Utils.createIdWorkerHome();
    for (File lockFile : idWorkerHome.listFiles()) {
      // check the format like 10.142.1.151.lock.0001
        if (!lockFile.getName().startsWith(ipudotlock)) {
            continue;
        }

      String workerId = lockFile.getName().substring(workerIdIndex);
        if (!workerId.matches("\\d\\d\\d\\d")) {
            continue;
        }

        if (sb.length() > 0) {
            sb.append(',');
        }
      sb.append(workerId);
    }

    return sb.toString();
  }


  /**
   * Find the local available worker id.
   *
   * @return -1 when N/A
   */
  private long findAvailWorkerId() {
    File idWorkerHome = Utils.createIdWorkerHome();

    for (File lockFile : idWorkerHome.listFiles()) {
      // check the format like 10.142.1.151.lock.0001
        if (!lockFile.getName().startsWith(ipudotlock)) {
            continue;
        }

      String workerId = lockFile.getName().substring(workerIdIndex);
        if (!workerId.matches("\\d\\d\\d\\d")) {
            continue;
        }

      FileLock fileLock = new FileLock(lockFile);
      if (!fileLock.tryLock()) {
        fileLock.destroy();
        continue;
      }

      this.fileLock = fileLock;
      return Long.parseLong(workerId);
    }

    return -1;
  }

  @Override
  public void initialize() {
      if (inited) {
          return;
      }
    init();
    this.inited = true;
  }

  @Override
  public long availableWorkerId() {
    return workerId;
  }

  @Override
  public void release() {
      if (fileLock != null) {
          fileLock.destroy();
      }
    inited = false;
  }
}
